#include "reporter.h"
#include "QtCore/qdiriterator.h"
#include "results/resultsjsinterface.h"
#include "data/datasetpackage.h"
#include "utilities/messageforwarder.h"
#include "gui/preferencesmodel.h"
#include "analysis/analyses.h"
#include <iostream>
#include <QDateTime>
#include <QFile>
#include <QDirIterator>
#include <QStringRef>
#include "tempfiles.h"
#include "log.h"

Reporter::Reporter(QObject *parent, QDir reportingDir) 
	: _reportingDir(reportingDir), 
	  _pdfPath(_reportingDir.absoluteFilePath("report.pdf"))
{
	assert(_reporter == nullptr);
	_reporter = this;

	QObject::connect(ResultsJsInterface::singleton(), &ResultsJsInterface::pdfPrintingFinished, this, &Reporter::onPdfPrintingFinishedHandler, Qt::UniqueConnection);
	//because of the connection exporting to pdf from the filemenu/results won't work anymore... 
	//but this is only used when JASP is running in reporting mode so that doesnt matter
}

Reporter * Reporter::_reporter = nullptr;

Reporter * Reporter::reporter() { return _reporter; }

bool Reporter::isJaspFileNotDabaseOrSynching() const
{
	//We report through cerr because otherwise it might get messy if JASP is started hidden from a service.
	//The service will just have to catch the output from std::cerr
	if(!DataSetPackage::pkg()->isLoaded() || !DataSetPackage::pkg()->hasDataSet())
	{
		std::cerr << "There is no file loaded or it has no data..." << std::endl;
		return false;
	}
	
	if(DataSetPackage::pkg()->isDatabase())
	{
		if(!DataSetPackage::pkg()->isDatabaseSynching())
		{
			std::cerr << "A non synching database file was loaded, which means there will be no updates..." << std::endl;
			return false;		
		}
	}
	else
	{
		if(DataSetPackage::pkg()->dataFilePath() == "")
		{
			std::cerr << "A jasp file without a datafile was loaded, which means there can be no updates..." << std::endl;

			return MessageForwarder::showYesNo(tr("No datafile"),
											   tr("JASP was started in reporting mode, but the jasp file is not coupled to a datafile.\n"
												  "This means there can be no updates...\n\n"
												  "Would you like to continue anyway?"));
		}	
		
		//Lets make sure it actually checks whether the datafile is  being synched or not.
		DataSetPackage::pkg()->setSynchingExternally(true);
	}
			
	return true;
}

void Reporter::analysesFinished()
{
	//The order in which the following are executed is important.
	//The report service will use the "modified" dates of the files created by each of these to keep an eye on JASP.
	//so, when "checkReports() write "sometingToReport.txt" the service knows a refresh has just completed
	//and when "report.complete" is written it knows everything is ready to be sent/read/whatever

	if(PreferencesModel::prefs()->reportingMode())
	{
		checkReports();
		writeResultsJson();
		writeReportLog();
		writeReport();
	}
}

/// Goes through all analyses' results and extracts those parts generated by jaspReport and assings as an array to _reports
bool Reporter::checkReports()
{
	Json::Value reports		= Json::arrayValue;
	_reportsNeeded  = 0;
	_reportsNeutral = 0;

	Analyses::analyses()->applyToAll([&](Analysis * a)
	{
		int reportsNeeded, reportsNeutral;
		Json::Value analysisReports = reportsFromAnalysis(a, reportsNeeded, reportsNeutral);
		_reportsNeeded  += reportsNeeded;
		_reportsNeutral += reportsNeutral;
		
		if(analysisReports.size() || a->isErrorState())
		{
			Json::Value analysisReport = Json::objectValue;
			
			analysisReport["id"]		= int(a->id());
			analysisReport["name"]		= a->name();
			analysisReport["title"]		= a->title();
			analysisReport["module"]	= a->module();
			analysisReport["reports"]	= analysisReports;
			
			if(a->isErrorState())
			{
				analysisReport["error"]			= a->results().get("error", false).asBool();
				analysisReport["errorMessage"]	= a->results().get("errorMessage", "???").asString();
				//lets keep it concise analysisReport["results"]		= a->results();
			}
			
			reports.append(analysisReport);
		}
	});
	
	_reports = reports;
	
	QFile somethingToReportFile(_reportingDir.absoluteFilePath("reportSomething.txt"));
	
	if(somethingToReportFile.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text))
		somethingToReportFile.write(("Needed reports: " + std::to_string(_reportsNeeded)).c_str()); //Just something simple to check, could be anything really.

	return _reportsNeeded;
}

Json::Value Reporter::reportsFromAnalysis(Analysis * a, int & reportsNeeded, int & reportsNeutral)
{
	reportsNeeded  = 0;
	reportsNeutral = 0;

	std::function<void(stringvec & names, const Json::Value & meta)> reportNameExtractor =
		[&reportNameExtractor](stringvec & names, const Json::Value & meta) -> void
		{
			switch(meta.type())
			{
			case Json::arrayValue:
				for(const Json::Value & subEntry : meta)
					reportNameExtractor(names, subEntry);
				break;

			case Json::objectValue:
			{
				if(meta.get("type", "").asString() == "reportNode")
					names.push_back(meta["name"].asString());

				if(meta.get("type", "").asString() == "collection")
				{
					const Json::Value & collection = meta.get("meta", Json::arrayValue);

					for(const Json::Value & subEntry : collection)
						reportNameExtractor(names, subEntry);
				}

				break;
			}

			default:
				//Why would we even be here? It is just to shut up the warning ;)
				break;
			}
		};

	std::function<void(const stringset & names, const Json::Value & results, Json::Value & reports)> reportsExtractor =
		[&reportsExtractor, &reportsNeeded, &reportsNeutral](const stringset & names, const Json::Value & results, Json::Value & reports) -> void
		{
			if(names.size() == 0)
				return;

			switch(results.type())
			{
			case Json::arrayValue:
				for(const Json::Value & subEntry : results)
					reportsExtractor(names, subEntry, reports);
				break;

			case Json::objectValue:
				for(const std::string & name : results.getMemberNames())
					if(names.count(name))
					{
						const Json::Value & report = results[name];
						if(report["report"].asBool())	reportsNeeded++;
						else							reportsNeutral++;

						reports.append(report);
					}
					else if(name == "collection")
						reportsExtractor(names, results[name], reports);
					else if(results[name].isObject() && results[name].isMember("collection"))
						reportsExtractor(names, results[name]["collection"], reports);
				break;


			default:
				//Why would we even be here? It is just to shut up the warning ;)
				break;
			}
		};


	Json::Value analysisReports = Json::arrayValue;

	stringvec reportNames;
	reportNameExtractor(reportNames, a->resultsMeta());

	//for(const std::string & reportName : reportNames)
		//Log::log() << "found reportName: " << reportName << std::endl;

	reportsExtractor(stringset(reportNames.begin(), reportNames.end()), a->results(), analysisReports);

	return analysisReports;
}

bool Reporter::analysisHasReportNeeded(Analysis *a)
{
	int needed, neutral;
	reportsFromAnalysis(a, needed, neutral);
	return needed;
}

QDir Reporter::dashboardDir() const
{
	return _reportingDir.absoluteFilePath("dashboard/");
}

void Reporter::exportDashboard()
{
	QDir dashboardDir(Reporter::dashboardDir());

	if(dashboardDir.exists("index.html"))
		return;

	dashboardDir.mkpath(".");

	copyQDirRecursively(QDir(":/html/"), dashboardDir);
}

void Reporter::writeResultsJson()
{
	exportDashboard();

	QFile resultsFile(Reporter::dashboardDir().absoluteFilePath("results.json"));
	
	if(resultsFile.open(QIODevice::WriteOnly | QIODevice::Truncate  | QIODevice::Text))
		resultsFile.write(Analyses::analyses()->asJson() .toStyledString().c_str());

	//Also copy the resources to the dashboarddir so we can show the operator some pictures
	copyQDirRecursively(QDir(tq(TempFiles::sessionDirName() + "/resources/")), dashboardDir().absoluteFilePath("resources"));
}

void Reporter::writeReport()
{
	ResultsJsInterface::singleton()->exportToPDF(_pdfPath);
}

void Reporter::writeReportLog()
{
	QFile reportLogFile(_reportingDir.absoluteFilePath("reportLog.csv"));

	if(!reportLogFile.exists())
	{
		reportLogFile.open(QIODevice::WriteOnly | QIODevice::Truncate  | QIODevice::Text);
		reportLogFile.write("DateTime,NeutralReports,ImportantReports\n");
		reportLogFile.close();
	}

	reportLogFile.open(QIODevice::WriteOnly | QIODevice::Append  | QIODevice::Text);
	QStringList writeThis = {
		'"' + QDateTime::currentDateTimeUtc().toString() + '"',
		tq(std::to_string(_reportsNeutral)),
		tq(std::to_string(_reportsNeeded))
	};
	reportLogFile.write((writeThis.join(',').toStdString()+"\n").c_str());
	reportLogFile.close();
}


///Called from ResultsJSInterface and before that WebEngine and makes sure "report.complete" is written/updated to signal to the service some new data is available
void Reporter::onPdfPrintingFinishedHandler(QString pdfPath)
{
	if(_pdfPath != pdfPath) 
	{
		std::cerr << "Got unexpected Reporter::onPdfPrintingFinishedHandler! Expected path: \"" << _pdfPath << "\" but got: \"" << pdfPath << "\"...\nIgnoring it!" << std::endl;
		return;
	}
	
	QFile reportComplete(_reportingDir.absoluteFilePath("report.complete"));
	
	if(reportComplete.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text))
		reportComplete.write(QDateTime::currentDateTimeUtc().toString(Qt::ISODate).toStdString().c_str());
}
